Passed
Branch wavefile-rw (d3e828)
by Rafael S.
02:30
created

WaveFile.fromIMAADPCM   A

Complexity

Conditions 2

Size

Total Lines 11
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 9
dl 0
loc 11
rs 9.95
c 0
b 0
f 0
1
/*
2
 * Copyright (c) 2017-2019 Rafael da Silva Rocha.
3
 *
4
 * Permission is hereby granted, free of charge, to any person obtaining
5
 * a copy of this software and associated documentation files (the
6
 * "Software"), to deal in the Software without restriction, including
7
 * without limitation the rights to use, copy, modify, merge, publish,
8
 * distribute, sublicense, and/or sell copies of the Software, and to
9
 * permit persons to whom the Software is furnished to do so, subject to
10
 * the following conditions:
11
 *
12
 * The above copyright notice and this permission notice shall be
13
 * included in all copies or substantial portions of the Software.
14
 *
15
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
 *
23
 */
24
25
/**
26
 * @fileoverview The WaveFile class.
27
 * @see https://github.com/rochars/wavefile
28
 */
29
30
/** @module wavefile */
31
32
import bitDepthLib from 'bitdepth';
33
import * as imaadpcm from 'imaadpcm';
34
import * as alawmulaw from 'alawmulaw';
35
import {encode, decode} from 'base64-arraybuffer-es6';
36
import WaveFileCreator from './lib/wavefile-creator';
37
import truncateSamples from './lib/truncate-samples';
38
import fixRIFFTag from './lib/fix-riff-tag';
39
import {unpackArray, unpackArrayTo} from 'byte-data';
40
41
/**
42
 * A class to manipulate wav files.
43
 */
44
export default class WaveFile extends WaveFileCreator {
45
46
  /**
47
   * Force a file as RIFF.
48
   */
49
  toRIFF() {
50
    this.fromScratch(
51
      this.fmt.numChannels,
52
      this.fmt.sampleRate,
53
      this.bitDepth,
54
      unpackArray(this.data.samples, this.dataType));
55
  }
56
57
  /**
58
   * Force a file as RIFX.
59
   */
60
  toRIFX() {
61
    this.fromScratch(
62
      this.fmt.numChannels,
63
      this.fmt.sampleRate,
64
      this.bitDepth,
65
      unpackArray(this.data.samples, this.dataType),
66
      {container: 'RIFX'});
67
  }
68
69
  /**
70
   * Encode a 16-bit wave file as 4-bit IMA ADPCM.
71
   * @throws {Error} If sample rate is not 8000.
72
   * @throws {Error} If number of channels is not 1.
73
   */
74
  toIMAADPCM() {
75
    if (this.fmt.sampleRate !== 8000) {
76
      throw new Error(
77
        'Only 8000 Hz files can be compressed as IMA-ADPCM.');
78
    } else if (this.fmt.numChannels !== 1) {
79
      throw new Error(
80
        'Only mono files can be compressed as IMA-ADPCM.');
81
    } else {
82
      this.assure16Bit_();
83
      /** @type {!Int16Array} */
84
      let output = new Int16Array(this.data.samples.length / 2);
85
      unpackArrayTo(this.data.samples, this.dataType, output);
86
      this.fromScratch(
87
        this.fmt.numChannels,
88
        this.fmt.sampleRate,
89
        '4',
90
        imaadpcm.encode(output),
91
        {container: this.correctContainer_()});
92
    }
93
  }
94
95
  /**
96
   * Decode a 4-bit IMA ADPCM wave file as a 16-bit wave file.
97
   * @param {string} bitDepthCode The new bit depth of the samples.
98
   *    One of '8' ... '32' (integers), '32f' or '64' (floats).
99
   *    Optional. Default is 16.
100
   */
101
  fromIMAADPCM(bitDepthCode='16') {
102
    this.fromScratch(
103
      this.fmt.numChannels,
104
      this.fmt.sampleRate,
105
      '16',
106
      imaadpcm.decode(this.data.samples, this.fmt.blockAlign),
107
      {container: this.correctContainer_()});
108
    if (bitDepthCode != '16') {
109
      this.toBitDepth(bitDepthCode);
110
    }
111
  }
112
113
  /**
114
   * Encode a 16-bit wave file as 8-bit A-Law.
115
   */
116
  toALaw() {
117
    this.assure16Bit_();
118
    /** @type {!Int16Array} */
119
    let output = new Int16Array(this.data.samples.length / 2);
120
    unpackArrayTo(this.data.samples, this.dataType, output);
121
    this.fromScratch(
122
      this.fmt.numChannels,
123
      this.fmt.sampleRate,
124
      '8a',
125
      alawmulaw.alaw.encode(output),
126
      {container: this.correctContainer_()});
127
  }
128
129
  /**
130
   * Decode a 8-bit A-Law wave file into a 16-bit wave file.
131
   * @param {string} bitDepthCode The new bit depth of the samples.
132
   *    One of '8' ... '32' (integers), '32f' or '64' (floats).
133
   *    Optional. Default is 16.
134
   */
135
  fromALaw(bitDepthCode='16') {
136
    this.fromScratch(
137
      this.fmt.numChannels,
138
      this.fmt.sampleRate,
139
      '16',
140
      alawmulaw.alaw.decode(this.data.samples),
141
      {container: this.correctContainer_()});
142
    if (bitDepthCode != '16') {
143
      this.toBitDepth(bitDepthCode);
144
    }
145
  }
146
147
  /**
148
   * Encode 16-bit wave file as 8-bit mu-Law.
149
   */
150
  toMuLaw() {
151
    this.assure16Bit_();
152
    /** @type {!Int16Array} */
153
    let output = new Int16Array(this.data.samples.length / 2);
154
    unpackArrayTo(this.data.samples, this.dataType, output);
155
    this.fromScratch(
156
      this.fmt.numChannels,
157
      this.fmt.sampleRate,
158
      '8m',
159
      alawmulaw.mulaw.encode(output),
160
      {container: this.correctContainer_()});
161
  }
162
163
  /**
164
   * Decode a 8-bit mu-Law wave file into a 16-bit wave file.
165
   * @param {string} bitDepthCode The new bit depth of the samples.
166
   *    One of '8' ... '32' (integers), '32f' or '64' (floats).
167
   *    Optional. Default is 16.
168
   */
169
  fromMuLaw(bitDepthCode='16') {
170
    this.fromScratch(
171
      this.fmt.numChannels,
172
      this.fmt.sampleRate,
173
      '16',
174
      alawmulaw.mulaw.decode(this.data.samples),
175
      {container: this.correctContainer_()});
176
    if (bitDepthCode != '16') {
177
      this.toBitDepth(bitDepthCode);
178
    }
179
  }
180
181
  /**
182
   * Use a .wav file encoded as a base64 string to load the WaveFile object.
183
   * @param {string} base64String A .wav file as a base64 string.
184
   * @throws {Error} If any property of the object appears invalid.
185
   */
186
  fromBase64(base64String) {
187
    this.fromBuffer(new Uint8Array(decode(base64String)));
188
  }
189
190
  /**
191
   * Return a base64 string representig the WaveFile object as a .wav file.
192
   * @return {string} A .wav file as a base64 string.
193
   * @throws {Error} If any property of the object appears invalid.
194
   */
195
  toBase64() {
196
    /** @type {!Uint8Array} */
197
    let buffer = this.toBuffer();
198
    return encode(buffer, 0, buffer.length);
199
  }
200
201
  /**
202
   * Return a DataURI string representig the WaveFile object as a .wav file.
203
   * The return of this method can be used to load the audio in browsers.
204
   * @return {string} A .wav file as a DataURI.
205
   * @throws {Error} If any property of the object appears invalid.
206
   */
207
  toDataURI() {
208
    return 'data:audio/wav;base64,' + this.toBase64();
209
  }
210
211
  /**
212
   * Use a .wav file encoded as a DataURI to load the WaveFile object.
213
   * @param {string} dataURI A .wav file as DataURI.
214
   * @throws {Error} If any property of the object appears invalid.
215
   */
216
  fromDataURI(dataURI) {
217
    this.fromBase64(dataURI.replace('data:audio/wav;base64,', ''));
218
  }
219
220
  /**
221
   * Change the bit depth of the samples.
222
   * @param {string} newBitDepth The new bit depth of the samples.
223
   *    One of '8' ... '32' (integers), '32f' or '64' (floats)
224
   * @param {boolean} changeResolution A boolean indicating if the
225
   *    resolution of samples should be actually changed or not.
226
   * @throws {Error} If the bit depth is not valid.
227
   */
228
  toBitDepth(newBitDepth, changeResolution=true) {
229
    /** @type {string} */
230
    let toBitDepth = newBitDepth;
231
    /** @type {string} */
232
    let thisBitDepth = this.bitDepth;
233
    if (!changeResolution) {
234
      if (newBitDepth != '32f') {
235
        toBitDepth = this.dataType.bits.toString();
236
      }
237
      thisBitDepth = this.dataType.bits;
238
    }
239
    this.assureUncompressed_();
240
    /** @type {number} */
241
    let sampleCount = this.data.samples.length / (this.dataType.bits / 8);
242
    /** @type {!Float64Array} */
243
    let typedSamplesInput = new Float64Array(sampleCount);
244
    /** @type {!Float64Array} */
245
    let typedSamplesOutput = new Float64Array(sampleCount);
246
    unpackArrayTo(this.data.samples, this.dataType, typedSamplesInput);
247
    if (thisBitDepth == "32f" || thisBitDepth == "64") {
248
      truncateSamples(typedSamplesInput);
249
    }
250
    bitDepthLib(
251
      typedSamplesInput, thisBitDepth, toBitDepth, typedSamplesOutput);
252
    this.fromScratch(
253
      this.fmt.numChannels,
254
      this.fmt.sampleRate,
255
      newBitDepth,
256
      typedSamplesOutput,
257
      {container: this.correctContainer_()});
258
  }
259
260
  /**
261
   * Write a RIFF tag in the INFO chunk. If the tag do not exist,
262
   * then it is created. It if exists, it is overwritten.
263
   * @param {string} tag The tag name.
264
   * @param {string} value The tag value.
265
   * @throws {Error} If the tag name is not valid.
266
   */
267
  setTag(tag, value) {
268
    tag = fixRIFFTag(tag);
269
    /** @type {!Object} */
270
    let index = this.getTagIndex_(tag);
271
    if (index.TAG !== null) {
272
      this.LIST[index.LIST].subChunks[index.TAG].chunkSize =
273
        value.length + 1;
274
      this.LIST[index.LIST].subChunks[index.TAG].value = value;
275
    } else if (index.LIST !== null) {
276
      this.LIST[index.LIST].subChunks.push({
277
        chunkId: tag,
278
        chunkSize: value.length + 1,
279
        value: value});
280
    } else {
281
      this.LIST.push({
282
        chunkId: 'LIST',
283
        chunkSize: 8 + value.length + 1,
284
        format: 'INFO',
285
        subChunks: []});
286
      this.LIST[this.LIST.length - 1].subChunks.push({
287
        chunkId: tag,
288
        chunkSize: value.length + 1,
289
        value: value});
290
    }
291
  }
292
293
  /**
294
   * Return the value of a RIFF tag in the INFO chunk.
295
   * @param {string} tag The tag name.
296
   * @return {?string} The value if the tag is found, null otherwise.
297
   */
298
  getTag(tag) {
299
    /** @type {!Object} */
300
    let index = this.getTagIndex_(tag);
301
    if (index.TAG !== null) {
302
      return this.LIST[index.LIST].subChunks[index.TAG].value;
303
    }
304
    return null;
305
  }
306
307
  /**
308
   * Return a Object<tag, value> with the RIFF tags in the file.
309
   * @return {!Object<string, string>} The file tags.
310
   */
311
  listTags() {
312
    /** @type {?number} */
313
    let index = this.getLISTINFOIndex_();
314
    /** @type {!Object} */
315
    let tags = {};
316
    if (index !== null) {
317
      for (let i = 0, len = this.LIST[index].subChunks.length; i < len; i++) {
318
        tags[this.LIST[index].subChunks[i].chunkId] =
319
          this.LIST[index].subChunks[i].value;
320
      }
321
    }
322
    return tags;
323
  }
324
325
  /**
326
   * Remove a RIFF tag from the INFO chunk.
327
   * @param {string} tag The tag name.
328
   * @return {boolean} True if a tag was deleted.
329
   */
330
  deleteTag(tag) {
331
    /** @type {!Object} */
332
    let index = this.getTagIndex_(tag);
333
    if (index.TAG !== null) {
334
      this.LIST[index.LIST].subChunks.splice(index.TAG, 1);
335
      return true;
336
    }
337
    return false;
338
  }
339
340
  /**
341
   * Create a cue point in the wave file.
342
   * @param {number} position The cue point position in milliseconds.
343
   * @param {string} labl The LIST adtl labl text of the marker. Optional.
344
   */
345
  setCuePoint(position, labl='') {
346
    this.cue.chunkId = 'cue ';
347
    position = (position * this.fmt.sampleRate) / 1000;
348
    /** @type {!Array<!Object>} */
349
    let existingPoints = this.getCuePoints_();
350
    this.clearLISTadtl_();
351
    /** @type {number} */
352
    let len = this.cue.points.length;
353
    this.cue.points = [];
354
    /** @type {boolean} */
355
    let hasSet = false;
356
    if (len === 0) {
357
      this.setCuePoint_(position, 1, labl);
358
    } else {
359
      for (let i = 0; i < len; i++) {
360
        if (existingPoints[i].dwPosition > position && !hasSet) {
361
          this.setCuePoint_(position, i + 1, labl);
362
          this.setCuePoint_(
363
            existingPoints[i].dwPosition,
364
            i + 2,
365
            existingPoints[i].label);
366
          hasSet = true;
367
        } else {
368
          this.setCuePoint_(
369
            existingPoints[i].dwPosition,
370
            i + 1,
371
            existingPoints[i].label);
372
        }
373
      }
374
      if (!hasSet) {
375
        this.setCuePoint_(position, this.cue.points.length + 1, labl);
376
      }
377
    }
378
    this.cue.dwCuePoints = this.cue.points.length;
379
  }
380
381
  /**
382
   * Remove a cue point from a wave file.
383
   * @param {number} index the index of the point. First is 1,
384
   *    second is 2, and so on.
385
   */
386
  deleteCuePoint(index) {
387
    this.cue.chunkId = 'cue ';
388
    /** @type {!Array<!Object>} */
389
    let existingPoints = this.getCuePoints_();
390
    this.clearLISTadtl_();
391
    /** @type {number} */
392
    let len = this.cue.points.length;
393
    this.cue.points = [];
394
    for (let i = 0; i < len; i++) {
395
      if (i + 1 !== index) {
396
        this.setCuePoint_(
397
          existingPoints[i].dwPosition,
398
          i + 1,
399
          existingPoints[i].label);
400
      }
401
    }
402
    this.cue.dwCuePoints = this.cue.points.length;
403
    if (this.cue.dwCuePoints) {
404
      this.cue.chunkId = 'cue ';
405
    } else {
406
      this.cue.chunkId = '';
407
      this.clearLISTadtl_();
408
    }
409
  }
410
411
  /**
412
   * Return an array with all cue points in the file, in the order they appear
413
   * in the file.
414
   * The difference between this method and using the list in WaveFile.cue
415
   * is that the return value of this method includes the position in
416
   * milliseconds of each cue point (WaveFile.cue only have the sample offset)
417
   * @return {!Array<!Object>}
418
   */
419
  listCuePoints() {
420
    /** @type {!Array<!Object>} */
421
    let points = this.getCuePoints_();
422
    for (let i = 0, len = points.length; i < len; i++) {
423
      points[i].milliseconds =
424
        (points[i].dwPosition / this.fmt.sampleRate) * 1000;
425
    }
426
    return points;
427
  }
428
429
  /**
430
   * Update the label of a cue point.
431
   * @param {number} pointIndex The ID of the cue point.
432
   * @param {string} label The new text for the label.
433
   */
434
  updateLabel(pointIndex, label) {
435
    /** @type {?number} */
436
    let cIndex = this.getAdtlChunk_();
437
    if (cIndex !== null) {
438
      for (let i = 0, len = this.LIST[cIndex].subChunks.length; i < len; i++) {
439
        if (this.LIST[cIndex].subChunks[i].dwName ==
440
            pointIndex) {
441
          this.LIST[cIndex].subChunks[i].value = label;
442
        }
443
      }
444
    }
445
  }
446
447
  /**
448
   * Make the file 16-bit if it is not.
449
   * @private
450
   */
451
  assure16Bit_() {
452
    this.assureUncompressed_();
453
    if (this.bitDepth != '16') {
454
      this.toBitDepth('16');
455
    }
456
  }
457
458
  /**
459
   * Uncompress the samples in case of a compressed file.
460
   * @private
461
   */
462
  assureUncompressed_() {
463
    if (this.bitDepth == '8a') {
464
      this.fromALaw();
465
    } else if (this.bitDepth == '8m') {
466
      this.fromMuLaw();
467
    } else if (this.bitDepth == '4') {
468
      this.fromIMAADPCM();
469
    }
470
  }
471
  
472
  /**
473
   * Push a new cue point in this.cue.points.
474
   * @param {number} position The position in milliseconds.
475
   * @param {number} dwName the dwName of the cue point
476
   * @private
477
   */
478
  setCuePoint_(position, dwName, label) {
479
    this.cue.points.push({
480
      dwName: dwName,
481
      dwPosition: position,
482
      fccChunk: 'data',
483
      dwChunkStart: 0,
484
      dwBlockStart: 0,
485
      dwSampleOffset: position,
486
    });
487
    this.setLabl_(dwName, label);
488
  }
489
490
  /**
491
   * Return an array with all cue points in the file, in the order they appear
492
   * in the file.
493
   * @return {!Array<!Object>}
494
   * @private
495
   */
496
  getCuePoints_() {
497
    /** @type {!Array<!Object>} */
498
    let points = [];
499
    for (let i = 0, len = this.cue.points.length; i < len; i++) {
500
      points.push({
501
        dwPosition: this.cue.points[i].dwPosition,
502
        label: this.getLabelForCuePoint_(
503
          this.cue.points[i].dwName)});
504
    }
505
    return points;
506
  }
507
508
  /**
509
   * Return the label of a cue point.
510
   * @param {number} pointDwName The ID of the cue point.
511
   * @return {string}
512
   * @private
513
   */
514
  getLabelForCuePoint_(pointDwName) {
515
    /** @type {?number} */
516
    let cIndex = this.getAdtlChunk_();
517
    if (cIndex !== null) {
518
      for (let i = 0, len = this.LIST[cIndex].subChunks.length; i < len; i++) {
519
        if (this.LIST[cIndex].subChunks[i].dwName ==
520
            pointDwName) {
521
          return this.LIST[cIndex].subChunks[i].value;
522
        }
523
      }
524
    }
525
    return '';
526
  }
527
528
  /**
529
   * Clear any LIST chunk labeled as 'adtl'.
530
   * @private
531
   */
532
  clearLISTadtl_() {
533
    for (let i = 0, len = this.LIST.length; i < len; i++) {
534
      if (this.LIST[i].format == 'adtl') {
535
        this.LIST.splice(i);
536
      }
537
    }
538
  }
539
540
  /**
541
   * Create a new 'labl' subchunk in a 'LIST' chunk of type 'adtl'.
542
   * @param {number} dwName The ID of the cue point.
543
   * @param {string} label The label for the cue point.
544
   * @private
545
   */
546
  setLabl_(dwName, label) {
547
    /** @type {?number} */
548
    let adtlIndex = this.getAdtlChunk_();
549
    if (adtlIndex === null) {
550
      this.LIST.push({
551
        chunkId: 'LIST',
552
        chunkSize: 4,
553
        format: 'adtl',
554
        subChunks: []});
555
      adtlIndex = this.LIST.length - 1;
556
    }
557
    this.setLabelText_(adtlIndex === null ? 0 : adtlIndex, dwName, label);
558
  }
559
560
  /**
561
   * Create a new 'labl' subchunk in a 'LIST' chunk of type 'adtl'.
562
   * @param {number} adtlIndex The index of the 'adtl' LIST in this.LIST.
563
   * @param {number} dwName The ID of the cue point.
564
   * @param {string} label The label for the cue point.
565
   * @private
566
   */
567
  setLabelText_(adtlIndex, dwName, label) {
568
    this.LIST[adtlIndex].subChunks.push({
569
      chunkId: 'labl',
570
      chunkSize: label.length,
571
      dwName: dwName,
572
      value: label
573
    });
574
    this.LIST[adtlIndex].chunkSize += label.length + 4 + 4 + 4 + 1;
575
  }
576
577
  /**
578
   * Return the index of the 'adtl' LIST in this.LIST.
579
   * @return {?number}
580
   * @private
581
   */
582
  getAdtlChunk_() {
583
    for (let i = 0, len = this.LIST.length; i < len; i++) {
584
      if (this.LIST[i].format == 'adtl') {
585
        return i;
586
      }
587
    }
588
    return null;
589
  }
590
591
  /**
592
   * Return the index of the INFO chunk in the LIST chunk.
593
   * @return {?number} the index of the INFO chunk.
594
   * @private
595
   */
596
  getLISTINFOIndex_() {
597
    /** @type {?number} */
598
    let index = null;
599
    for (let i = 0, len = this.LIST.length; i < len; i++) {
600
      if (this.LIST[i].format === 'INFO') {
601
        index = i;
602
        break;
603
      }
604
    }
605
    return index;
606
  }
607
608
  /**
609
   * Return the index of a tag in a FILE chunk.
610
   * @param {string} tag The tag name.
611
   * @return {!Object<string, ?number>}
612
   *    Object.LIST is the INFO index in LIST
613
   *    Object.TAG is the tag index in the INFO
614
   * @private
615
   */
616
  getTagIndex_(tag) {
617
    /** @type {!Object<string, ?number>} */
618
    let index = {LIST: null, TAG: null};
619
    for (let i = 0, len = this.LIST.length; i < len; i++) {
620
      if (this.LIST[i].format == 'INFO') {
621
        index.LIST = i;
622
        for (let j=0, subLen = this.LIST[i].subChunks.length; j < subLen; j++) {
623
          if (this.LIST[i].subChunks[j].chunkId == tag) {
624
            index.TAG = j;
625
            break;
626
          }
627
        }
628
        break;
629
      }
630
    }
631
    return index;
632
  }
633
634
  /**
635
   * Return 'RIFF' if the container is 'RF64', the current container name
636
   * otherwise. Used to enforce 'RIFF' when RF64 is not allowed.
637
   * @return {string}
638
   * @private
639
   */
640
  correctContainer_() {
641
    return this.container == 'RF64' ? 'RIFF' : this.container;
642
  }
643
}
644